/*****************************************************************************
 *   Copyright 2005 Tim A Wang
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 ****************************************************************************/
package com.jb.framework.util;

import com.jb.util.ClassUtil;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.sql.ResultSet;
import java.sql.Timestamp;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * Property utility that provides service for inspection of a java bean.
 * It uses apache beanutil for most of its work.
 *
 * @author anping wang
 * @version 1.0
 */
public class PropUtil {
    static private Log log = LogFactory.getLog(com.jb.framework.util.PropUtil.class);

    /**
     * Returns a list of constructors for a give class name.
     *
     * @param className The name of the class.
     *
     * @return List of constrcutors.
     */
    public static List getConstructors(String className) {
        try {
            Class clazz = ClassUtil.getClass(className);
            List list = new ArrayList();
            Constructor[] methods = clazz.getConstructors();

            for (int i = 0; i < methods.length; i++) {
                list.add(methods[i]);
            }

            return list;
        } catch (Exception e) {
            return Collections.EMPTY_LIST;
        }
    }

    /**
     * Returns a list of properties for a give class. The property must have
     * both read and write methods.
     *
     * @param className the class name.
     *
     * @return List of properties.
     */
    public static List<PropertyDescriptor> getProperties(Class className) {
        ArrayList<PropertyDescriptor> hm = new ArrayList<PropertyDescriptor>();
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(className);

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propReadMethod = properties[i].getReadMethod();
                Method propWriteMethod = properties[i].getWriteMethod();

                if ((propReadMethod != null) && (propWriteMethod != null)) {
                    hm.add(properties[i]);
                }
            }
        }

        return hm;
    }
    
    
    public static List getGetters(Class className) {
        ArrayList hm = new ArrayList();
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(className);

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propReadMethod = properties[i].getReadMethod();
                
                if ((propReadMethod != null)) {
                    hm.add(properties[i]);
                }
            }
        }

        return hm;
    }
    /**
     * Sets bean property value.
     *
     * @param bean whose value to be set.
     * @param name property name.
     * @param value property value.
     *
     * @return true if value is set successfully.
     */
    public static boolean setProperty(Object bean, String name, Object value) {
        try {
            if (bean == null) {
                return false;
            }

            PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(bean, name);

            if (descriptor != null) {
                Class propType = descriptor.getPropertyType();
                Object propValue = null;

                if (propType.isInstance(value)) {
                    propValue = value;
                } else {
                    propValue = getMappedValue(propType, value);
                }

                if (propValue == null) {
                    return false;
                }

                if (propType.isInstance(propValue) || propType.isPrimitive()) {
                    PropertyUtils.setProperty(bean, name, propValue);

                    return true;
                } else if (log.isDebugEnabled()) {
                    log.debug("Property type not match " + bean.getClass() + " " + name + "=" + propValue);
                }

                return false;
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Property not found " + bean.getClass() + " " + name + "=" + value);
                }
            }
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error("Failed to set property " + bean.getClass() + " " + name + "=" + value, e);
            }
        }

        return false;
    }

    private static Map cachedReadMethods = new Hashtable(); 
    
    public static Object getProperty(Object object, String propertyName)
        throws IntrospectionException, IllegalAccessException, InvocationTargetException {
        String cacheKey =object.getClass()+propertyName;
        Method method = (Method)cachedReadMethods.get(cacheKey);
        if(method==null)
        {
            PropertyDescriptor pd = new PropertyDescriptor(propertyName, object.getClass());
            method =pd.getReadMethod();
            cachedReadMethods.put(cacheKey, method);
        }
        return method.invoke(object, new Object[] {  });
    }
    /**
     * Returns property descriptors for a given class.
     *
     * @param className the name of the class.
     *
     * @return property desciptors hashed against property name.
     *
     * @throws IllegalArgumentException DOCUMENT ME!
     */
    public static HashMap getPropertyDescriptorsMap(Class className) {
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(className);

        HashMap hm = new HashMap();

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propReadMethod = properties[i].getReadMethod();
                Method propWriteMethod = properties[i].getWriteMethod();

                if ((propReadMethod != null) && (propWriteMethod != null)) {
                    try {
                        hm.put(propName, properties[i]);
                    } catch (Exception e) {
                        throw new IllegalArgumentException("Unable to retrieve '" + propName + "' property " +
                            "value: " + e.getMessage() + ".");
                    }
                }
            }
        }

        return hm;
    }

    /**
     * Returns a list of property names.
     *
     * @param className the name of the class.
     *
     * @return List of property name.
     */
    public static List getPropertyNames(Class className) {
        ArrayList hm = new ArrayList();
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(className);

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propReadMethod = properties[i].getReadMethod();
                Method propWriteMethod = properties[i].getWriteMethod();

                if ((propReadMethod != null) && (propWriteMethod != null)) {
                    hm.add(propName);
                }
            }
        }

        return hm;
    }

    public static HashMap getPropertyValues(Class javaType, Object src) {
        HashMap hm = new HashMap();

        if (src == null) {
            return hm;
        }

        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(javaType);

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propReadMethod = properties[i].getReadMethod();
                Method propWriteMethod = properties[i].getWriteMethod();

                //property has both sets and sets
                if ((propReadMethod != null) && (propWriteMethod != null)) {
                    Object propValue = null;

                    try {
                        propValue = getProperty(src, propName);

                        if (propValue != null) {
                            hm.put(propName, propValue);
                        }
                    } catch (Exception e) {
                        if(log.isErrorEnabled())
                            log.error(PropUtil.class, e);
                    }
                }
            }
        }

        return hm;
    }

    /**
     * DOCUMENT ME!
     *
     * @param src parameter
     * @param dest parameter
     *
     * @throws IllegalArgumentException DOCUMENT ME!
     */
    public static void assignProperties(Object src, Object dest) {
        if ((src == null) || (dest == null)) {
            throw new IllegalArgumentException("Source and Destination object must be provided for assignProperties");
        }

        //dest must be type of src object for assign
        if (!src.getClass().isInstance(dest)) {
            return;
        }

        HashMap hm = getPropertyValues(src.getClass(), src);
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(dest.getClass());

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propWriteMethod = properties[i].getWriteMethod();

                if ((propWriteMethod != null)) {
                    try {
                        Object propValue = hm.get(propName);

                        //we only deal with setters, not generic methods
                        if (propValue != null) {
                            Class paramType = propWriteMethod.getParameterTypes()[0];

                            if (paramType.isPrimitive() || paramType.equals(propValue.getClass())) {
                                propWriteMethod.invoke(dest, new Object[] { propValue });
                            }
                        }
                    } catch (Exception e) {
                        if(log.isErrorEnabled())
                            log.error(PropUtil.class, e);
                    }
                }
            }
        }
    }

    /**
     * Assigns object properties in a set of name value pairs in the map.
     * The key in the value map matches object property name, the value 
     * in the hashmap will be assigned to the object. Values are coersed into
     * the property type using EL.
     *
     * @param dest object instance to be assigned value to.
     * @param valueMap hashmap where properties are stored.
     *
     * @throws IllegalArgumentException if valueMap or dest are null
     */
    public static void assignPropertiesFromMap(Object dest, Map valueMap) {
        if ((valueMap == null) || (dest == null)) {
            throw new IllegalArgumentException("Source and Destination object must be provided for assignProperties");
        }

        Map hm = valueMap;
        PropertyDescriptor[] properties = PropertyUtils.getPropertyDescriptors(dest.getClass());

        for (int i = 0; i < properties.length; i++) {
            String propName = properties[i].getName();
            Class propType = properties[i].getPropertyType();

            if ((propType != null) && isPropertyTypeSupported(propType, propName)) {
                Method propWriteMethod = properties[i].getWriteMethod();

                if ((propWriteMethod != null)) {
                    try {
                        Object propValue = hm.get(propName);

                        if ((propValue != null) && !propType.isInstance(propValue)) {
                            propValue = com.jb.faces.el.AttributeEvaluator.convertValue(propValue.toString(), propType);
                        }

                        if (propValue != null) {
                            Class paramType = propWriteMethod.getParameterTypes()[0];

                            if (paramType.isPrimitive()) {
                                propWriteMethod.invoke(dest, new Object[] { propValue });
                            } else if (paramType.equals(propValue.getClass())) {
                                propWriteMethod.invoke(dest, new Object[] { propValue });
                            }
                        }
                    } catch (Exception e) {
                        if (log.isErrorEnabled()) {
                            log.error("Failed " + propWriteMethod.getName() + " for " + dest.getClass(), e);
                        }
                    }
                }
            }
        }
    }

    /**
     * Executes a named method on a bean, The method must not have arguments.
     *
     * @param bean whose method is to be invoked. If it is type of String, it
     * is treated as class name, and static method is executed on this class.
     * @param methodName of method to be invoked.
     *
     * @throws Exception if fails to execute the method.
     *
     */
    public static Object invoke(Object bean, String methodName)
        throws ExecutionException {
        try {
            if (bean instanceof String) //classname, this will execute static method
             {
                Class clazz = ClassUtil.getClass((String) bean);

                return clazz.getMethod(methodName, new Class[] {  }).invoke(null, new Object[] {  });
            } else {
                return bean.getClass().getMethod(methodName, new Class[] {  }).invoke(bean, new Object[] {  });
            }
        } catch (Exception e) {
            throw new ExecutionException(e.getMessage());
        }
    }

    /**
     * Executes a named method on a bean, The method must have just one argument.
     *
     * @param bean whose method is to be invoked. If it is type of String, it
     * is treated as class name, and static method is executed on this class.
     * @param methodName of method to be invoked.
     *
     * @throws Exception if fails to execute the method.
     *
     */
    public static Object invoke2(Object bean, String methodName, Object argument)
        throws ExecutionException {
        try {
            if ((argument == null) || (bean == null)) {
                return null; //throw new ExecutionException("Argument is required for this function");
            }

            if (bean instanceof String) //classname, this will execute static method
             {
                Class clazz = ClassUtil.getClass((String) bean);

                return clazz.getMethod(methodName, new Class[] { argument.getClass() }).invoke(null,
                    new Object[] { argument });
            } else {
                return bean.getClass().getMethod(methodName, new Class[] { argument.getClass() }).invoke(bean,
                    new Object[] { argument });
            }
        } catch (Exception e) {
            if(log.isErrorEnabled())
                log.error(PropUtil.class, e);

            return null;

            //throw new ExecutionException(e.getMessage());
        }
    }

    public static void updateWithResultSet(Object root, ResultSet rs) {
        try {
            HashMap hm = com.jb.framework.util.PropUtil.getPropertyDescriptorsMap(root.getClass());
            Iterator it = hm.keySet().iterator();

            while (it.hasNext()) {
                String pName = (String) it.next();
                PropertyDescriptor pd = (PropertyDescriptor) hm.get(pName);

                try {
                    Object pValue = null;

                    //set value
                    if (pd.getPropertyType() == Date.class) {
                        Timestamp ts = rs.getTimestamp(pName);

                        if (ts != null) {
                            pValue = new Date(ts.getTime());
                        }
                    }

                    if (pd.getPropertyType() == String.class) {
                        pValue = rs.getString(pName);
                    }

                    if ((pd.getPropertyType() == long.class) || (pd.getPropertyType() == Long.class)) {
                        pValue = new Long(rs.getLong(pName));
                    }

                    if ((pd.getPropertyType() == int.class) || (pd.getPropertyType() == Integer.class)) {
                        pValue = new Integer(rs.getInt(pName));
                    }

                    if ((pd.getPropertyType() == float.class) || (pd.getPropertyType() == Float.class)) {
                        pValue = new Float(rs.getFloat(pName));
                    }

                    if ((pd.getPropertyType() == double.class) || (pd.getPropertyType() == Double.class)) {
                        pValue = new Double(rs.getDouble(pName));
                    }

                    if ((pd.getPropertyType() == boolean.class) || (pd.getPropertyType() == Boolean.class)) {
                        pValue = new Boolean(rs.getBoolean(pName));
                    }

                    if (pValue != null) {
                        com.jb.framework.util.PropUtil.setProperty(root, pName, pValue);
                    }
                } catch (Exception e) {
                    if(log.isErrorEnabled()) {
                        log.error("PropUtil.updateWithResultSet", e);
                    }
                }
            }
        } catch (Exception e) {
            if(log.isErrorEnabled()) {
                log.error("PropUtil.updateWithResultSet", e);
            }
        }
    }
    //determine if the property type is supported for conversion.
    private static boolean isPropertyTypeSupported(Class classToTest, String propName) {
        return (classToTest == Date.class) || classToTest.isPrimitive() || (classToTest == Integer.class) ||
        (classToTest == String.class) || (classToTest == Calendar.class) || (classToTest == Double.class) ||
        (classToTest == Float.class) || (classToTest == Boolean.class) || (classToTest == Long.class);
    }

    private static  Object getMappedValue(Class type, Object value) {
        if (value == null) {
            return null;
        }

        return ConvertUtils.convert(value.toString(), type);
    }
}
